知识篇 -- Vue3组件通信详解
在Vue3应用中,组件化是核心开发模式。组件之间的数据传递和事件交互是构建复杂应用不可或缺的部分。Vue3提供了多种组件通信方式,以适应不同场景的需求,从父子组件的直接通信到跨层级、非亲缘组件的复杂数据流管理。理解并熟练运用这些通信模式,是构建高效、可维护Vue3应用的关键。
# Props / Emits:父子组件通信的基石 父子通信
# 1. Props:父组件向子组件传递数据
Props 是父组件向子组件传递数据的主要方式。子组件通过 defineProps 宏函数声明接收的属性。
- 单向数据流:Props是单向的,父组件更新Props,子组件会随之更新,但子组件不能直接修改Props。
- 类型校验:可以对Props进行类型、默认值、是否必需等校验,增强组件的健壮性。
示例:
<!-- ParentComponent.vue -->
<template>
<ChildComponent :message="parentMessage" :count="10" @update-count="handleUpdateCount" />
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const parentMessage = ref('Hello from Parent!');
const handleUpdateCount = (newCount) => {
console.log('Received new count:', newCount);
// 可以更新父组件的状态
};
</script>
<!-- ChildComponent.vue -->
<template>
<div>
<p>{{ message }}</p>
<p>Count: {{ count }}</p>
<button @click="emitUpdate">Update Count</button>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
message: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
});
const emit = defineEmits(['update-count']);
const emitUpdate = () => {
emit('update-count', props.count + 1);
};
</script>
# 2. Emits:子组件向父组件触发事件
Emits 是子组件向父组件发送消息(触发事件)的方式。子组件通过 defineEmits 宏函数声明可以触发的事件,并通过 emit 函数触发。
- 事件参数:
emit函数可以携带参数,传递给父组件的事件处理函数。 - 事件校验:可以对Emits事件进行校验。
示例:见上文Props示例。
# v-model:双向绑定的语法糖 双向绑定
v-model 是Vue中实现组件双向绑定的语法糖。在自定义组件上使用 v-model,它会默认绑定 modelValue prop 和 update:modelValue 事件。
示例:
<!-- ParentComponent.vue -->
<template>
<MyInput v-model="searchText" />
<p>Search Text: {{ searchText }}</p>
</template>
<script setup>
import { ref } from 'vue';
import MyInput from './MyInput.vue';
const searchText = ref('');
</script>
<!-- MyInput.vue -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
defineProps({
modelValue: String
});
defineEmits(['update:modelValue']);
</script>
- 多个
v-model:Vue3支持在同一个组件上使用多个v-model,通过v-model:propName语法指定绑定的属性名。
# Provide / Inject:跨层级通信 跨层级
Provide 和 Inject 允许祖先组件向其所有后代组件(无论层级多深)提供数据,而无需通过中间组件层层传递Props。
provide():在祖先组件中提供数据。inject():在后代组件中注入数据。- 响应性:提供的数据可以是响应式的,后代组件可以响应其变化。
示例:
<!-- AncestorComponent.vue -->
<template>
<MiddleComponent />
</template>
<script setup>
import { provide, ref } from 'vue';
import MiddleComponent from './MiddleComponent.vue';
const theme = ref('dark');
provide('app-theme', theme); // 提供响应式数据
setTimeout(() => {
theme.value = 'light'; // 2秒后改变主题
}, 2000);
</script>
<!-- MiddleComponent.vue (无需传递) -->
<template>
<DescendantComponent />
</template>
<script setup>
import DescendantComponent from './DescendantComponent.vue';
</script>
<!-- DescendantComponent.vue -->
<template>
<p>Current Theme: {{ injectedTheme }}</p>
</template>
<script setup>
import { inject } from 'vue';
const injectedTheme = inject('app-theme', 'default'); // 注入数据,可提供默认值
</script>
# EventBus (mitt):非亲缘组件通信 全局事件
EventBus 是一种发布/订阅模式,适用于任何非亲缘关系组件之间的通信。在Vue3中,通常使用轻量级的库如 mitt 或 tiny-emitter 来实现。
- 优点:简单灵活,适用于小型项目或少量全局事件。
- 缺点:难以追踪事件来源和处理,容易导致维护困难,不推荐用于大型复杂项目。
示例 (使用 mitt):
// eventBus.js
import mitt from 'mitt';
const emitter = mitt();
export default emitter;
// ComponentA.vue
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script setup>
import emitter from './eventBus';
const sendMessage = () => {
emitter.emit('custom-event', 'Hello from Component A!');
};
</script>
// ComponentB.vue
<template>
<p>Received: {{ receivedMessage }}</p>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import emitter from './eventBus';
const receivedMessage = ref('');
onMounted(() => {
emitter.on('custom-event', (message) => {
receivedMessage.value = message;
});
});
onUnmounted(() => {
emitter.off('custom-event'); // 组件销毁时移除监听
});
</script>
# Vuex / Pinia:集中式状态管理 状态管理
对于大型复杂应用,使用Vuex(Vue2)或Pinia(Vue3推荐)进行集中式状态管理是最佳实践。它们提供了一个可预测的状态容器,方便管理应用的所有状态。
- 优点:
- 集中管理:所有组件的状态都存储在一个地方,易于追踪和调试。
- 可预测性:通过严格的规则(如Mutation、Action),确保状态变更的可预测性。
- 工具支持:提供强大的开发者工具,方便状态的调试和时间旅行。
- 应用场景:大型单页应用(SPA)中的全局状态,如用户认证信息、购物车、主题设置等。
示例 (Pinia):
// stores/counter.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++;
}
},
getters: {
doubleCount: (state) => state.count * 2
}
});
// ComponentA.vue
<template>
<p>Count: {{ counter.count }}</p>
<button @click="counter.increment()">Increment</button>
</template>
<script setup>
import { useCounterStore } from './stores/counter';
const counter = useCounterStore();
</script>
# useAttrs 和 useSlots:透传属性与插槽 透传
在 <script setup> 中,useAttrs 和 useSlots 宏函数提供了访问组件实例的透传属性和插槽的能力。
useAttrs():获取父组件传递给子组件但未被子组件Props声明的属性。useSlots():获取父组件传递给子组件的所有插槽内容。
示例:
<!-- ParentComponent.vue -->
<template>
<MyButton class="custom-btn" data-id="123">
<span>Click Me</span>
<template #icon>🚀</template>
</MyButton>
</template>
<script setup>
import MyButton from './MyButton.vue';
</script>
<!-- MyButton.vue -->
<template>
<button v-bind="attrs">
<slot name="icon"></slot>
<slot></slot>
</button>
</template>
<script setup>
import { useAttrs, useSlots } from 'vue';
// attrs 包含了 class="custom-btn" 和 data-id="123"
const attrs = useAttrs();
const slots = useSlots();
console.log(attrs); // { class: 'custom-btn', data-id: '123' }
console.log(slots.icon); // 插槽内容
</script>
# 总结
Vue3提供了全面而灵活的组件通信机制,从Props/Emits的父子通信,到v-model的双向绑定,再到Provide/Inject的跨层级通信,以及EventBus和状态管理库(Vuex/Pinia)的非亲缘通信。此外,useAttrs 和 useSlots 提供了对透传属性和插槽的细粒度控制。合理选择和组合这些通信方式,能够帮助开发者构建出结构清晰、数据流可控、易于维护和扩展的Vue3应用。